/*
* This source is part of the
* _____ ___ ____
* __ / / _ \/ _ | / __/___ _______ _
* / // / , _/ __ |/ _/_/ _ \/ __/ _ `/
* \___/_/|_/_/ |_/_/ (_)___/_/ \_, /
* /___/
* repository.
*
* Copyright (C) 2013 Benoit 'BoD' Lubek (BoD@JRAF.org)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jraf.android.bikey.backend.heartrate;
import java.util.List;
import java.util.UUID;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Build;
import org.jraf.android.bikey.app.Application;
import org.jraf.android.util.listeners.Listeners;
import org.jraf.android.util.log.Log;
import org.jraf.android.util.log.LogUtil;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public class HeartRateManagerJellyBeanMR2 extends HeartRateManager {
// See https://developer.bluetooth.org/gatt/services/Pages/ServiceViewer.aspx?u=org.bluetooth.service.heart_rate.xml
private static final int GATT_SERVICE_HEART_RATE = 0x180D;
// See https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml
private static final int GATT_CHARACTERISTIC_HEART_RATE_MEASUREMENT = 0x2A37;
// See https://developer.bluetooth.org/gatt/descriptors/Pages/DescriptorViewer.aspx?u=org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml
private static final int GATT_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION = 0x2902;
// @formatter:off
private static enum Status {
DISCONNECTED,
CONNECTING,
CONNECTED,
}
// @formatter:on
private Context mContext;
private BluetoothDevice mBluetoothDevice;
private BluetoothGatt mBluetoothGatt;
private Listeners<HeartRateListener> mListeners = new Listeners<>();
private int mLastValue = -1;
private Status mStatus = Status.DISCONNECTED;
/* package */HeartRateManagerJellyBeanMR2() {
mContext = Application.getApplication();
}
@Override
public void addListener(HeartRateListener listener) {
mListeners.add(listener);
}
@Override
public void removeListener(HeartRateListener listener) {
mListeners.remove(listener);
}
@Override
public void setBluetoothDevice(BluetoothDevice bluetoothDevice) {
Log.d();
mStatus = Status.CONNECTING;
// Inform listeners
mListeners.dispatch(HeartRateListener::onConnecting);
mBluetoothDevice = bluetoothDevice;
mBluetoothGatt = mBluetoothDevice.connectGatt(mContext, true, mBluetoothGattCallback);
}
private BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
Log.d("status=" + LogUtil.getConstantName(BluetoothGatt.class, status, "GATT_") + " newState="
+ LogUtil.getConstantName(BluetoothProfile.class, newState, "STATE_"));
switch (newState) {
case BluetoothProfile.STATE_CONNECTED:
mBluetoothGatt.discoverServices();
break;
case BluetoothProfile.STATE_DISCONNECTED:
onDisconnect();
break;
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
Log.d("status=" + LogUtil.getConstantName(BluetoothGatt.class, status, "GATT_"));
List<BluetoothGattService> services = gatt.getServices();
boolean found = false;
for (BluetoothGattService service : services) {
Log.d(service.getUuid().toString());
if (getAssignedNumber(service.getUuid()) == GATT_SERVICE_HEART_RATE) {
// Found heart rate service
onHeartRateServiceFound(service);
found = true;
break;
}
}
if (!found) {
onHeartRateServiceNotFound();
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
Log.d("characteristic=" + characteristic.getUuid());
int format;
int flag = characteristic.getProperties();
if ((flag & 0x01) != 0) {
format = BluetoothGattCharacteristic.FORMAT_UINT16;
} else {
format = BluetoothGattCharacteristic.FORMAT_UINT8;
}
int previousValue = mLastValue;
int value = characteristic.getIntValue(format, 1);
if (value < 50) {
// This is probably a false measurement, consider this as a disconnect
if (mStatus == Status.CONNECTED) {
// Disconnect
onDisconnect();
}
return;
}
mLastValue = value;
Log.d("heartRate=" + mLastValue);
if (mStatus != Status.CONNECTED) {
mStatus = Status.CONNECTED;
// Inform listeners
mListeners.dispatch(HeartRateListener::onConnected);
}
if (previousValue != mLastValue) {
// Inform listeners
mListeners.dispatch(listener -> listener.onHeartRateChange(mLastValue));
}
}
};
private void onDisconnect() {
mStatus = Status.DISCONNECTED;
mLastValue = -1;
mListeners.dispatch(HeartRateListener::onDisconnected);
}
/*
* Service.
*/
protected void onHeartRateServiceFound(BluetoothGattService service) {
Log.d();
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
Log.d("characteristic=" + characteristic.getUuid());
boolean found = false;
if (getAssignedNumber(characteristic.getUuid()) == GATT_CHARACTERISTIC_HEART_RATE_MEASUREMENT) {
// Found heart read measurement characteristic
onHeartRateMeasurementCharacteristicFound(characteristic);
found = true;
break;
}
if (!found) {
onHeartRateMeasurementCharacteristicNotFound();
}
}
}
protected void onHeartRateServiceNotFound() {
Log.d();
onError();
}
private void onError() {
mListeners.dispatch(HeartRateListener::onError);
}
/*
* Characteristic.
*/
private void onHeartRateMeasurementCharacteristicFound(BluetoothGattCharacteristic characteristic) {
Log.d();
boolean found = false;
for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) {
Log.d("descriptor=" + descriptor.getUuid());
if (getAssignedNumber(descriptor.getUuid()) == GATT_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION) {
onClientCharacteristicConfigurationDescriptorFound(characteristic, descriptor);
found = true;
break;
}
}
if (!found) {
onClientCharacteristicConfigurationDescriptorNotFound();
}
}
private void onHeartRateMeasurementCharacteristicNotFound() {
Log.d();
onError();
}
/*
* Descriptor.
*/
private void onClientCharacteristicConfigurationDescriptorFound(BluetoothGattCharacteristic characteristic, BluetoothGattDescriptor descriptor) {
Log.d();
mBluetoothGatt.setCharacteristicNotification(characteristic, true);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
}
private void onClientCharacteristicConfigurationDescriptorNotFound() {
Log.d();
onError();
}
/*
* Helpers.
*/
private static int getAssignedNumber(UUID uuid) {
// Keep only the significant bits of the UUID
return (int) ((uuid.getMostSignificantBits() & 0x0000FFFF00000000L) >> 32);
}
@Override
public boolean isConnected() {
return mStatus == Status.CONNECTED;
}
@Override
public boolean isConnecting() {
return mStatus == Status.CONNECTING;
}
@Override
public int getLastValue() {
return mLastValue;
}
@Override
public void disconnect() {
Log.d();
if (mBluetoothGatt != null) {
mBluetoothGatt.close();
mBluetoothGatt.disconnect();
mBluetoothGatt = null;
}
mBluetoothDevice = null;
onDisconnect();
}
}